<?php

use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\MySQL\Connection;

require_once 'workerman/Autoloader.php';
require_once 'workerman/Mysql/src/Connection.php';
require_once 'config/config.php';
require_once 'LogFactory.php';
//设置时区
date_default_timezone_set('Asia/Shanghai');
//获取日志类
$logger = LogFactory::getLogger('default');
// 初始化一个worker容器，监听1234端口,并且采用text协议，在一个包结束后面加上\n换行符号
$worker = new Worker(START_FOREIGN_IP);
$worker->name = 'server';
// windows环境这里进程数必须设置为1
$worker->count = 1;
// 新增加一个属性，用来保存uid到connection的映射
$worker->uidConnections = array();

/**
 * 广播灭灯
 */
const COMMAND_CODE_TURN_OFF_ALL_LIGHT = 0;
/**
 *  配置设备地址
 */
const COMMAND_CODE_SETTING_LIGHT = 1;
/**
 * 控制灯
 */
const COMMAND_CODE_CONTROL_LIGHT = 2;
/**
 * 灯质检
 */
const COMMAND_CODE_SELF_CHECK_LIGHT = 3;

/**
 * worker进程启动后建立一个内部通讯端口,方便将数据发送到客户端
 * 两种方式实现服务器向客户端发送数据，
 * 1.php通过curl访问到这个通讯端口，然后将数据post提交过来，读取数据是通过$_POST
 * 2.php通过socket连接到这个通讯端口，再写到缓存区，这边读取数据是通过$buffer
 * 这里使用的是方法1来发送数据给客户端
 * 注释时间：2017-12-20
 * @param $worker
 */
$worker->onWorkerStart = function ($worker) use ($logger) {
    global $db;
    $db = new Connection(START_DB_HOST, START_DB_PORT, START_DB_USER, START_DB_PASS, START_DB_NAME);
    $innerWorker = new Worker(START_INNER_IP);
    $innerWorker->onMessage = function ($connection, $buffer) use ($logger, $worker) {
        global $worker;
        $data = $_POST;
        if (empty($data)) {
            return;
        }
        $type = $data['type'];
        switch ($type) {
            case 'send':
                var_dump($data);
                //发送数据给客户端,其中\n为一个包的结束
                $command = $data['command'];
                $uids = json_decode($data['uid'], true);
                //将发送的数据写到日志中
                $logger->info('接受到的数据为:' . var_export($data, true));
                //发送数据
                foreach ($uids as $sendUid) {
                    $ret = sendMessageByUid($sendUid, $command);
                    $logger->info("发送给客户端: $sendUid 结果为: $ret");
                }
                $connection->send("ok");
                break;
            default:
                $connection->send('fail');
                break;
        }
        $connection->send('ok');
    };
    $innerWorker->onWorkerStop = function ($innerWorker) use ($logger) {
        $logger->error("内置进程停止运行");
    };
    $innerWorker->listen();
    //检测心跳
    Timer::add(0.5, function () use ($worker) {
        $time_now = time();
        foreach ($worker->connections as $connection) {
            // 有可能该connection还没收到过消息，则lastMessageTime设置为当前时间
            if (empty($connection->lastMessageTime)) {
                $connection->lastMessageTime = $time_now;
                continue;
            }
            // 上次通讯时间间隔大于心跳间隔，则认为客户端已经下线，关闭连接
            if ($time_now - $connection->lastMessageTime > HEARTBEAT_TIME) {
                if (isset($connection->uid)) {
                    // 连接断开时删除映射
                    unset($worker->uidConnections[$connection->uid]);
                }
                $connection->close();
            }
        }
        //每天定时进行质检

    });
};
/**
 * 当有客户端发来消息时执行的回调函数
 * 里面主要有两个操作
 * 1.处理每盏灯返回的数据，推送给服务器，通知服务器该命令已经执行完成
 * 2.处理自检返回的数据，写到数据库中
 * 其实还有一些其他比较隐蔽的操作，这对实际上的调试等等会有大的阻碍，而且实际上不做也是可以的操作
 * 1.绑定用户（有些逻辑要处理好，这边只是简单的处理，将心跳包发送过来的uis当作是这个客户端的uid绑定用户）
 * 注释时间：2017-12-20
 * @param $connection
 * @param $data
 */
$worker->onMessage = function ($connection, $data) use ($worker, $logger) {
    global $db;
    $connection->lastMessageTime = time();
    $logger->debug("接受的数据为：" . $data);
    echo date('Y-m-d H:i:s', time()) . '##### data is: ' . $data . "\n";
    //客户端返回的结果
    if (isset($connection->uid) && is_json($data)) {
        $logger->info('from client' . $data);
        //如果是json数据进行额外的数据处理
        handleResponse($data);
    } elseif (!isset($connection->uid) && is_string($data)) {
        $uid = $data;
        //内部设定了uid，也就是说只能是一个连接了，不然其他连接也会把数据发送到那边
        $connection->uid = $uid;
        $worker->uidConnections[$uid] = $connection;
        handleClientOnline($uid);
    } else {
        //一般是心跳包信息，可以忽略了
    }
};
/**
 * 连接建立时执行的回调函数,此时只是三次握手成功，并没有发送数据
 *
 * @param $connection
 */
$worker->onConnect = function ($connection) use ($worker) {
    $message = array(
        'message' => 'success'
    );
    $connection->send(json_encode($message));
};

/**
 * 连接断开时执行的回调函数
 * 将绑定的数据去除
 * @param $connection
 */
$worker->onClose = function ($connection) use ($worker) {
    global $worker;
    if (isset($connection->uid)) {
        handleClientOnline($connection->uid,0);
        // 连接断开时删除映射
        unset($worker->uidConnections[$connection->uid]);

    }
};
/**
 * 客户端连接出现错误
 *
 * @param $connection
 * @param $code
 * @param $msg
 */
$worker->onError = function ($connection, $code, $msg) use ($logger) {
    $logger->error("连接的uid：{$connection->uid}出错，code：{$code},msg：{$msg}");
};
/**
 * 连接的发送缓冲区满
 *
 * @param $connection
 */
$worker->onBufferFull = function ($connection) use ($logger) {
    $logger->warn("连接的uid：{$connection->uid}应用缓冲区满了");
};

/**
 * 当链接的发送缓冲区被清空
 *
 * @param $connection
 */
$worker->onBufferDrain = function ($connection) use ($logger) {
    $logger->warn("连接的uid：{$connection->uid}缓冲区的数据全部发送完毕");
};

/**
 * 停止worker实例的情况下
 * @param $worker
 */
$worker->onWorkerStop = function ($worker) use ($logger) {
    $logger->error("worker进程退出");
};

/**
 *  向所有验证的用户推送数据(广播)
 * @param $message
 */
function broadcast($message)
{
    global $worker;
    foreach ($worker->uidConnections as $connection) {
        $connection->send($message);
    }
}

/**
 * 针对uid推送数据
 * @param $uid
 * @param $message
 * @return bool
 */
function sendMessageByUid($uid, $message)
{
    global $worker;
    if (isset($worker->uidConnections[$uid])) {
        $connection = $worker->uidConnections[$uid];
        $res = $connection->send($message);
        if ($res === false) {
            //对失败进行重新间隔0.2s发送3次
            for ($i = 0; $i < 3; $i++) {
                //sleep 200毫秒
                usleep(1000 * 200);
                $tmpConnection = $worker->uidConnections[$uid];
                if (empty($tmpConnection)) {
                    continue;
                }
                $tmp = $tmpConnection->send($message);
                if ($tmp !== false) {
                    return true;
                }
            }
            //三次后还是失败的话，那么就进行失败统计
            deal_send_fail_content($uid, $message);
            return false;
        } else {
            //包括了null 和 true的情况
            return true;
        }
    }
    return false;
}

/**
 * 处理客户端是否在线
 *
 * @param $uid
 * @param int $status
 */
function handleClientOnline($uid,$status = 1){
    global $db;
    $sql = sprintf("update iwh_client set is_online = %d where uid = '%s'",$status,$uid);
    $db->query($sql);
}
/**
 * 判断不是json字符串
 * @param $str
 * @return bool
 */
function is_json($str)
{

    return is_null(json_decode($str)) ? false : true;
}

/**
 * 处理返回的数据
 *
 * @param $data
 */
function handleResponse($data)
{
    global $db, $logger;
    $receive = json_decode($data, true);
    switch ($receive['commandCode']) {
        case COMMAND_CODE_TURN_OFF_ALL_LIGHT:
            break;
        case COMMAND_CODE_SETTING_LIGHT:
            break;
        case COMMAND_CODE_CONTROL_LIGHT:
            $seatId = $receive['address'] . $receive['position'];
            $status = $receive['status'];
            if (intval($status) === 0 || empty($status)) {
                //$logger->info($receive);
            } else {
                $equipmentId = substr($seatId, 0, 4);
                $position = hexdec(substr($seatId, 4, 2));
                $status = 1;
                deal_response_data($seatId, $status, $equipmentId, $position);
            }
            var_dump($seatId . ' and status is ' . $status);
            break;
        case COMMAND_CODE_SELF_CHECK_LIGHT:
            $equipmentId = $receive['address'];
            //16进制转为10进制函数hexdec
            //10进制转为16进制函数dechex
            //10进制转为2进制函数decbin
            //2进制转为10进制函数bindec
            //任意进制转换 base_convert(数据，多少进制，转为多少进制)
            $result = $receive['result'];
            $bin='';
            for ($j = 0;$j < strlen($result);$j+= 2) {
                $bin.= base_convert(substr($result,$j,2),16,2);
            }
            for ($i = 0; $i < 28; $i++) {
                $code = substr($bin, $i * 2, 2);
                $position = str_pad(strtoupper(dechex($i)), 2, '0', STR_PAD_LEFT);
                $seatId = $equipmentId . $position;
                if (intval($code) === 0) {
                    //灯是好的
                    $status = 0;
                    $logger->info("$seatId and status is：$status");
                    var_dump($seatId . ' and status is ' . $status);
                } else {
                    //灯是坏的
                    $status = 1;
                    $logger->warn("$seatId and status is：$status");
                    var_dump($seatId . ' and status is ' . $status);
                }
                $sql = sprintf("update iwh_seat set status='%d' where seat_id='%s'", intval($status), $seatId);
                $db->query($sql);
            }
            break;
        default:
            break;
    }
}

/**
 *  * 处理操作数据库语句
 * @param string $seatId
 * @param string $status
 * @param string $equipmentId
 * @param string $position
 * @param string $type 记录日志类型
 */
function deal_response_data($seatId = '', $status = '', $equipmentId = '', $position = '', $type = 'warn')
{
    global $db, $logger;
    if (empty($seatId) || !is_numeric($status)) {
        return;
    }
    $sql = sprintf("update iwh_seat set status='%d' where seat_id='%s'", intval($status), $seatId);
    $res = $db->query($sql);
    //写到日志中
    if ($type === 'warn') {
        $logger->warn($sql);
        $logger->warn("设备码为：{ $equipmentId} 的第 {($position + 1)}盏灯出现的故障");
    } elseif ($type === 'info') {
        $logger->info($sql);
        $logger->info("设备码为：{ $equipmentId} 的第 {($position + 1)}盏灯出现的故障");
    }

}

function cURL($url = '', $data = '', $isPost = true)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_HEADER, 0);
//  启用时会将服务器服务器返回的"Location: "放在header中递归的返回给服务器，
//  使用CURLOPT_MAXREDIRS可以限定递归返回的数量。
//    curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1);
//    curl_setopt($ch,CURLOPT_MAXREDIRS,2);
    if ($isPost) {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    }
    if (stripos($url, 'https') === 0) {
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);//请求https时
    }

    $output = curl_exec($ch);
    if ($output === false) {
        return curl_error($ch);
    }
    curl_close($ch);
    return $output;
}

/**
 * 发送post请求
 * @param $url
 * @param $data
 * @return bool|string
 */
function send_post($url, $data)
{
    $data = http_build_query($data);
    $options = array(
        'http' => array(
            'method' => 'POST',
            'header' => 'Content-type:application/x-www-form-urlencoded',
            'content' => $data,
            'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36',
            'timeout' => 15 * 60 // 超时时间（单位:s）
        )
    );
    $context = stream_context_create($options);
    $result = file_get_contents($url, false, $context);
    return $result;
}

/**
 * 记录发送失败的信息
 *
 * @param $uid
 * @param $content
 */
function deal_send_fail_content($uid, $content)
{
    global $db;
    $data = [
        'uid' => $uid,
        'content' => $content,
        'remark' => 'send fail',
        'status' => 0,
        'create_time' => time()
    ];
    // 插入
    $insetId = $db->insert('iwh_client_error_msg')->cols($data)->query();
}

// 运行所有的worker（其实当前只定义了一个）
Worker::runAll();